package pl.softech.oldchart;

import java.awt.Color;
import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Insets;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseWheelEvent;
import java.awt.font.TextLayout;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Line2D;
import java.awt.geom.Point2D;
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import javax.swing.JLayeredPane;
import javax.swing.JMenu;
import javax.swing.JMenuItem;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;

import pl.softech.oldchart.GraphLegendFactory.IGraphLegend;

public class Graph2D extends JLayeredPane {
	
	private static final long serialVersionUID = 1L;
	
	private enum Scale {
		SCALE01(0.01),
		SCALE10(0.1),
		SCALE20(0.2),
		SCALE50(0.5),
		SCALE100(1),
		SCALE120(1.2),
		SCALE150(1.5),
		SCALE170(1.7),
		SCALE180(1.8),
		SCALE190(1.9);
		
		private String str;
		public double ratio;
		
		private Scale(double scale) {
			this.ratio = scale;
			str = "" + scale * 100 + "%";
		}
		@Override
		public String toString() { return str; }
		
		public static Scale getInstance(String str) {
			for(Scale sc : Scale.values())
				if(sc.toString().equals(str))
					return sc;
			return null;
		} 
		
	};
	
	private Collection<IGraph2DModel> models;
	private GraphLegendFactory graphLegendFactory;
	private JPanel currentGraphLegend;
	
	private double width;
	private double height;
	private double xRatio;
	private double yRatio;
	private Point2D startViewCoordinate;
	private Point2D startModelCoordinate;
	
	private Color xAxisColor = Color.RED;
	private Color yAxisColor = Color.RED;
	
	private boolean computeMaxModelCoordinate = true;
	private boolean computeMinModelCoordinate = true;
	
	private boolean computeModel = true;
	
	private Point2D maxModelCoordinate;
	private Point2D minModelCoordinate;
	
	private boolean drawModelCoordinates = false;
	
	private Insets insets = new Insets(15, 60, 45, 15);
	
	private double pointRadius = 5;
	
	//Graph Labels section
	private Color xLabelColor = Color.BLACK;
	private Color yLabelColor = Color.BLUE;
	private double pointXAxisRadius = 6;
	private double pointYAxisRadius = 6;
	private boolean showXAxisLabels = true;
	private boolean showYAxisLabels = true;
	private boolean autoCreateXAxisLabels = false;
	private boolean autoCreateYAxisLabels = false;
	private NumberFormat numberFormat;
	private Font xAxisLabelsFont = new Font("Dialog", Font.PLAIN, 12);
	private Font yAxisLabelsFont = new Font("Dialog", Font.PLAIN, 12);
	
	private List<IGraphView> aviableViews;
	private IGraphToolKit graphToolKit;
	private IGraphView currentView;
	private boolean initRenderingHintsFlag = true;
	private double scale = 1;
	
	public Graph2D(IGraph2DModel model) {
		super();
		models = new LinkedList<IGraph2DModel>();
		models.add(model);
		init();
	}
	
	public Graph2D(Collection<IGraph2DModel> models) {
		super();
		this.models = models;
		init();
	}
	
	private void init() {
		setOpaque(true);
		setBackground(new Color(0xD8, 0xE2, 0xF1));
		setLayout(null);
		numberFormat = NumberFormat.getInstance();
		numberFormat.setMaximumFractionDigits(2);
		graphLegendFactory = new GraphLegendFactory(models);
		currentGraphLegend = graphLegendFactory.createSimpleGraphLegend();
		Dimension dim = currentGraphLegend.getPreferredSize();
		currentGraphLegend.setBounds((int) 0, (int) 0, (int) dim.getWidth(), (int) dim.getHeight());
		add(currentGraphLegend);
		moveToFront(currentGraphLegend);
		initViews();
		initPopupMenu();
		initLeftButtonAction();
		initWheelButtonAction();
	}
	
	private void initPopupMenu() {
		
		class PopupListener extends MouseAdapter {
	        JPopupMenu popup;

	        PopupListener(JPopupMenu popupMenu) {
	            popup = popupMenu;
	        }

	        public void mousePressed(MouseEvent e) {
	            maybeShowPopup(e);
	        }

	        public void mouseReleased(MouseEvent e) {
	            maybeShowPopup(e);
	        }
	        
	        private void maybeShowPopup(MouseEvent e) {
	            if (e.isPopupTrigger()) { 
	            	popup.show(e.getComponent(), e.getX(), e.getY());
	            }
	        }
	    }
		
		JPopupMenu popup = new JPopupMenu();
		JMenuItem scaleSetupItems = new JMenu("scale");
		scaleSetupItems.setMnemonic(KeyEvent.VK_S);
		ActionListener scaleSetupItemListener = new ActionListener() {

			@Override
			public void actionPerformed(ActionEvent e) {
				JMenuItem src = (JMenuItem) e.getSource();
				Scale sc = Scale.getInstance(src.getActionCommand());
				if(sc != null) {
					scale = sc.ratio;
					setScale(sc.ratio);
				}
				
			}
			
		};
		for(Scale sc: Scale.values()) {
			JMenuItem item = new JMenuItem(sc.toString());
			item.setActionCommand(sc.toString());
			item.addActionListener(scaleSetupItemListener);
			scaleSetupItems.add(item);
		}
		popup.add(scaleSetupItems);
		
		JMenuItem graphTypeItems = new JMenu("graph Type");
		ActionListener graphTypeItemListener = new ActionListener() {

			@Override
			public void actionPerformed(ActionEvent e) {
				JMenuItem src = (JMenuItem) e.getSource();
				for(IGraphView v : aviableViews)
					if(v.toString().equals(src.getActionCommand()))
						currentView = v;
				repaint();
				
			}
			
		};
		for(IGraphView v : aviableViews) {
			JMenuItem item = new JMenuItem(v.toString());
			item.setActionCommand(v.toString());
			item.addActionListener(graphTypeItemListener);
			graphTypeItems.add(item);
		}
		popup.add(graphTypeItems);
		
		addMouseListener(new PopupListener(popup));
		
	}
	
	private void initWheelButtonAction() {
		MouseAdapter ml = new MouseAdapter() {
			
			@Override
			public void mouseWheelMoved(MouseWheelEvent e) {
				int notches = e.getWheelRotation();
				scale += notches * 0.05;
				setScale(scale);
			}
		};
		addMouseWheelListener(ml);
	}
	
	private void initLeftButtonAction() {
	
		MouseAdapter ml = new MouseAdapter() {
			
			private Point beginPoint;
			private Point2D minModelCoordinateTemp;
			private Point2D maxModelCoordinateTemp;
			
			@Override
			public void mouseMoved(MouseEvent e) {
				if(beginPoint == null) return;
				Point endPoint = e.getLocationOnScreen();
				Point distance = new Point(endPoint.x - beginPoint.x, endPoint.y - beginPoint.y);
				
				  minModelCoordinate = new Point2D.Double(
						  minModelCoordinateTemp.getX() - distance.getX() / xRatio,
						  minModelCoordinateTemp.getY() + distance.getY() / yRatio
				  );
				  
				  maxModelCoordinate = new Point2D.Double(
						  maxModelCoordinateTemp.getX() - distance.getX() / xRatio,
						  maxModelCoordinateTemp.getY() + distance.getY() / yRatio
				  );
				recomputeModel();
			}
			
			@Override
			public void mousePressed(MouseEvent e) {
				
				if(e.getButton() == MouseEvent.BUTTON1) {
					if(beginPoint != null) {
						beginPoint = null;
						setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
						return;
					}
					beginPoint = e.getLocationOnScreen();
					
					setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
					minModelCoordinateTemp = minModelCoordinate;
					maxModelCoordinateTemp = maxModelCoordinate;
				}
			}
			
			@Override
			public void mouseExited(MouseEvent e) {
				beginPoint = null;
				setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
			}
			
		};
		
		addMouseListener(ml);
		addMouseMotionListener(ml);
	
	}
		
	private void initViews() {
		aviableViews = new ArrayList<IGraphView>();
		graphToolKit = new IGraphToolKit() {

			@Override
			public Point2D convertPointFromModelToView(Point2D point) { return coordFromModelToView(point);	}

			@Override
			public double getCoordinatePointRadius() { return pointRadius; }

			@Override
			public Point2D getStartViewCoordinate() { return startViewCoordinate; }

			@Override
			public boolean getDrawModelCoordinates() { return drawModelCoordinates; }

		}; 
		aviableViews.add(new LineGraphView());
		aviableViews.add(new BarGraphView());
		currentView = aviableViews.get(1);
	}
	
	private void initRenderingHints(Graphics2D g) {
		g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
	}
	
	private void computeMaxModelCoordinate() {
		
		double xMax = Double.MIN_VALUE;
		double yMax = Double.MIN_VALUE;

		for (IGraph2DModel model : models) {
			xMax = Math.max(xMax, model.getMaxX());
			yMax = Math.max(yMax, model.getMaxY());
		}
		maxModelCoordinate = new Point2D.Double(xMax, yMax);

	}
	
	private void computeMinModelCoordinate() {
		
		double xMin = Double.MAX_VALUE;
		double yMin = Double.MAX_VALUE;

		for (IGraph2DModel model : models) {
			xMin = Math.min(xMin, model.getMinX());
			yMin = Math.min(yMin, model.getMinY());
		}
		minModelCoordinate = new Point2D.Double(xMin, yMin);

	}
	
	private void autoCreateAxisLabels(Graphics2D g2) {
		
		double delta;
		NumberFormat nbf = NumberFormat.getInstance();
		nbf.setMaximumFractionDigits(2);
		
		if(showXAxisLabels && autoCreateXAxisLabels) {
			Map<Double, String> xLabels = new HashMap<Double, String>();
			xLabels.put(startModelCoordinate.getX(), numberFormat.format(startModelCoordinate.getX()) + "\n" + numberFormat.format(startModelCoordinate.getY()));
			delta = maxModelCoordinate.getX() - minModelCoordinate.getX();
			for(double x = minModelCoordinate.getX(); x <= maxModelCoordinate.getX(); x += delta / 4.0)
				if(x != startModelCoordinate.getX())
					xLabels.put(x, "" + nbf.format(x));
			drawXAxisLabels(g2, xLabels);
			
		}
		
		if(showYAxisLabels && autoCreateYAxisLabels) {
			Map<Double, String> yLabels = new HashMap<Double, String>();
			delta = maxModelCoordinate.getY() - minModelCoordinate.getY();
			for(double y = minModelCoordinate.getY(); y <= maxModelCoordinate.getY(); y += delta / 4.0)
				if(y != startModelCoordinate.getY())
					yLabels.put(y, "" + nbf.format(y));
			drawYAxisLabels(g2, yLabels);
			
		}
		
		
		
	}
	
	private void drawXAxisLabels(Graphics2D g2, Map<Double, String> labels) {
		Color color = g2.getColor();
		g2.setColor(xLabelColor);
		for (Double x : labels.keySet()) {
			
			Point2D p = coordFromModelToView(new Point2D.Double(x, startModelCoordinate.getY()));
			
			Ellipse2D ellipse = new Ellipse2D.Double(p.getX() - pointXAxisRadius / 2, 
					p.getY() - pointXAxisRadius / 2, pointXAxisRadius, pointXAxisRadius);
			
			g2.fill(ellipse);
			
			TextLayout txtLayout = new TextLayout(labels.get(x), xAxisLabelsFont, g2.getFontRenderContext());
			double xDrawCoord = p.getX() - txtLayout.getBounds().getWidth() / 2;
			double yDrawCoord = p.getY() + 2 * txtLayout.getBounds().getHeight();
			txtLayout.draw(g2, (float) xDrawCoord, (float) yDrawCoord);
			
		}
		
		g2.setColor(color);
	}
		
	
	private void drawYAxisLabels(Graphics2D g2, Map<Double, String> labels) {
		Color color = g2.getColor();
		g2.setColor(yLabelColor);
		for (Double y : labels.keySet()) {
			
			Point2D p = coordFromModelToView(new Point2D.Double(startModelCoordinate.getX(), y));
			
			Ellipse2D ellipse = new Ellipse2D.Double(p.getX() - pointYAxisRadius / 2, 
					p.getY() - pointYAxisRadius / 2, pointYAxisRadius, pointYAxisRadius);
			
			g2.fill(ellipse);
			
			TextLayout txtLayout = new TextLayout(labels.get(y), yAxisLabelsFont, g2.getFontRenderContext());
			double xDrawCoord = p.getX() - 1.2 * txtLayout.getBounds().getWidth();
			double yDrawCoord = p.getY() + txtLayout.getBounds().getHeight() / 2;
			txtLayout.draw(g2, (float) xDrawCoord, (float) yDrawCoord);
			
		}
		
		g2.setColor(color);
	}
	
	private void initModelLabels(Graphics2D g2, IGraph2DModel model) {
		Map<Double, String> xLabels = model.getXAxisLabels();
		Map<Double, String> yLabels = model.getYAxisLabels();
		
		// xAxis
		if(xLabels != null && showXAxisLabels && model.showXAxisLabels()) {
			drawXAxisLabels(g2, xLabels);
		}
		
		// yAxis
		if (yLabels != null && showYAxisLabels && model.showYAxisLabels()) {
			drawYAxisLabels(g2, yLabels);
		}
		
	}

	private void recomputeModel() {
		computeModel = true;
		computeMinModelCoordinate = false;
		computeMaxModelCoordinate = false;
		repaint();
	} 
	
//	private void recomputeAll() {
//		computeModel = true;
//		computeMinModelCoordinate = true;
//		computeMaxModelCoordinate = true;
//		repaint();
//	}
	
	private void computeModel(Rectangle rect) {
		
		if(computeMaxModelCoordinate)
			computeMaxModelCoordinate();
		if(computeMinModelCoordinate)
			computeMinModelCoordinate();
		
		computeMinModelCoordinate = false;
		computeMaxModelCoordinate = false;
		
		if(!computeModel) return;
		computeModel = false;
		
		double maxX = maxModelCoordinate.getX();
		double minX = minModelCoordinate.getX();
		double absX = maxX - minX;
		double xNegRatio = (minX < 0) ? -minX/absX : 0;
		
		double maxY = maxModelCoordinate.getY();
		double minY = minModelCoordinate.getY();
		double absY = maxY - minY;
		double yNegRatio = (minY < 0) ? -minY/absY : 0;
		
		startViewCoordinate = new Point2D.Double(
				xNegRatio * (double) rect.getWidth() + rect.getX(), 
				rect.getHeight() - yNegRatio * (double) rect.getHeight() + rect.getY()
			);
		
		startModelCoordinate = new Point2D.Double(
				(minX < 0) ? 0 : minX,
				(minY < 0) ? 0 : minY		
			);
		
		xRatio = (double) rect.getWidth() / absX;
		yRatio = (double) rect.getHeight() / absY;
		
	}
	
	private void drawAxis(Rectangle rect, Graphics2D g2) {
		
		computeModel(rect);
		
		Color color = g2.getColor();
		//xAxis
		g2.setColor(xAxisColor);
		g2.draw(new Line2D.Double(rect.getX(), startViewCoordinate.getY(), 
				rect.getWidth() + rect.getX(), startViewCoordinate.getY()));
		//yAxis
		g2.setColor(yAxisColor);
		g2.draw(new Line2D.Double(startViewCoordinate.getX(), rect.getY(), 
				startViewCoordinate.getX(), rect.getHeight() + rect.getY()));
		
		g2.setColor(color);
		
		//drawing Model Labels
		for(IGraph2DModel model : models)
			initModelLabels(g2, model);
		
		//drawing Graph Labels
		autoCreateAxisLabels(g2);
	}
	
	private Point2D coordFromModelToView(Point2D point) {
		return new Point2D.Double(
				startViewCoordinate.getX() + (point.getX() - startModelCoordinate.getX()) * xRatio,
				startViewCoordinate.getY() - (point.getY() - startModelCoordinate.getY()) * yRatio
				);
	}
	
	@Override
	public void paint(Graphics g) {
		super.paint(g);
		Graphics2D g2 = (Graphics2D) g;
		
		if(initRenderingHintsFlag) {
			initRenderingHints(g2);
			initRenderingHintsFlag = false;
		}
		
		final int width = getWidth();
		final int height = getHeight();
		
		
		if(this.width != width || this.height != height) {
			computeModel = true;
			computeMaxModelCoordinate = true;
			computeMinModelCoordinate = true;
			this.width = width;
			this.height = height;
		}
		
		final Rectangle rect = new Rectangle(
				insets.left, 
				insets.top, 
				width - (insets.left + insets.right), 
				height - (insets.top + insets.bottom)
		);
		
		drawAxis(rect, g2);
	
		for(IGraph2DModel model : models)
			currentView.drawGraph(g2, model, graphToolKit);
		
	}
	
  public void setShowXAxisLabels(boolean bFlag) { showXAxisLabels = bFlag; }
  
  public void setShowYAxisLabels(boolean bFlag) { showYAxisLabels = bFlag; }
  
  public void setInsets(Insets insets) { this.insets = insets; }
  
  public Insets getInsets() { return insets; }
	
  public void addModel(IGraph2DModel model) { 
	  
	  models.add(model);
	  ((IGraphLegend)currentGraphLegend).addModel(model);
	  repaint();
	  
  }
  
  public void removeModel(IGraph2DModel model) { 
	  
	  models.remove(model);
	  ((IGraphLegend)currentGraphLegend).removeModel(model);
	  repaint();
	  
  }
  
  public void setGraphLegendVisible(boolean isVisible) { currentGraphLegend.setVisible(isVisible); }
  
  public void setAutoCreateXAxisLabels(boolean bFlag) { autoCreateXAxisLabels = bFlag; }
  
  public void setAutoCreateYAxisLabels(boolean bFlag) { autoCreateYAxisLabels = bFlag; }
  
  public void setDrawModelCoordinates(boolean bFlag) { drawModelCoordinates = bFlag; }
  
  public void setScale(double ratio) {
	  
	  computeMaxModelCoordinate = true;
	  computeMaxModelCoordinate = true;
	  
	  computeMaxModelCoordinate();
	  computeMinModelCoordinate();
	  
	  computeMaxModelCoordinate = false;
	  computeMaxModelCoordinate = false;
	  
	  double absX = maxModelCoordinate.getX() - minModelCoordinate.getX();
	  double absY = maxModelCoordinate.getY() - minModelCoordinate.getY();
	  minModelCoordinate = new Point2D.Double(
			  minModelCoordinate.getX() - (1-ratio) * absX / 2,
			  minModelCoordinate.getY() - (1-ratio) * absY / 2
	  );
	  
	  maxModelCoordinate = new Point2D.Double(
			  maxModelCoordinate.getX() + (1-ratio) * absX / 2,
			  maxModelCoordinate.getY() + (1-ratio) * absY / 2
	  );
	  recomputeModel();
	  
  }
  
}
